package drds.connection_pool;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.io.Closeable;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.concurrent.atomic.AtomicBoolean;

public class ConnectionPool extends Configuration implements DataSource, Closeable
{
   private static final Logger LOGGER = LoggerFactory.getLogger(ConnectionPool.class);

   private final AtomicBoolean isShutdown = new AtomicBoolean();

   private final Pool fastPathPool;
   private volatile Pool pool;

   /**
    * Default constructor.  Setters are used to configure the proxy.  Using
    * this constructor vs. {@link #ConnectionPool(Configuration)} will
    * result in {@link #getConnection()} performance that is slightly lower
    * due to lazy initialization checks.
    * <p>
    * The first call to {@link #getConnection()} starts the proxy.  Once the proxy
    * is started, the configuration is "sealed" and no further configuration
    * changes are possible -- except via {@link } methods.
    */
   public ConnectionPool()
   {
      super();
      fastPathPool = null;
   }

   /**
    * Construct a ConnectionPool with the specified configuration.  The
    * {@link Configuration} is copied and the proxy is started by invoking this
    * constructor.
    * <p>
    * The {@link Configuration} can be modified without affecting the ConnectionPool
    * and used to initialize another ConnectionPool instance.
    *
    * @param configuration a Configuration instance
    */
   public ConnectionPool(Configuration configuration)
   {
      configuration.validate();
      configuration.copyStateTo(this);

      LOGGER.info("{} - Starting...", configuration.getPoolName());
      pool = fastPathPool = new Pool(this);
      LOGGER.info("{} - Start completed.", configuration.getPoolName());

      this.seal();
   }

   // ***********************************************************************
   //                          DataSource methods
   // ***********************************************************************

   /**
    * {@inheritDoc}
    */
   @Override
   public Connection getConnection() throws SQLException
   {
      if (isClosed())
      {
         throw new SQLException("ConnectionPool " + this + " has been closed.");
      }

      if (fastPathPool != null)
      {
         return fastPathPool.getConnection();
      }

      // See http://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
      Pool result = pool;
      if (result == null)
      {
         synchronized (this)
         {
            result = pool;
            if (result == null)
            {
               validate();
               LOGGER.info("{} - Starting...", getPoolName());
               try
               {
                  pool = result = new Pool(this);
                  this.seal();
               } catch (Pool.PoolInitializationException pie)
               {
                  if (pie.getCause() instanceof SQLException)
                  {
                     throw (SQLException) pie.getCause();
                  } else
                  {
                     throw pie;
                  }
               }
               LOGGER.info("{} - Start completed.", getPoolName());
            }
         }
      }

      return result.getConnection();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Connection getConnection(String username, String password) throws SQLException
   {
      throw new SQLFeatureNotSupportedException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public PrintWriter getLogWriter() throws SQLException
   {
      Pool p = pool;
      return (p != null ? p.getUnwrappedDataSource().getLogWriter() : null);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setLogWriter(PrintWriter out) throws SQLException
   {
      Pool p = pool;
      if (p != null)
      {
         p.getUnwrappedDataSource().setLogWriter(out);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public int getLoginTimeout() throws SQLException
   {
      Pool p = pool;
      return (p != null ? p.getUnwrappedDataSource().getLoginTimeout() : 0);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void setLoginTimeout(int seconds) throws SQLException
   {
      Pool p = pool;
      if (p != null)
      {
         p.getUnwrappedDataSource().setLoginTimeout(seconds);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException
   {
      throw new SQLFeatureNotSupportedException();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   @SuppressWarnings("unchecked")
   public <T> T unwrap(Class<T> iface) throws SQLException
   {
      if (iface.isInstance(this))
      {
         return (T) this;
      }

      Pool p = pool;
      if (p != null)
      {
         final DataSource unwrappedDataSource = p.getUnwrappedDataSource();
         if (iface.isInstance(unwrappedDataSource))
         {
            return (T) unwrappedDataSource;
         }

         if (unwrappedDataSource != null)
         {
            return unwrappedDataSource.unwrap(iface);
         }
      }

      throw new SQLException("Wrapped DataSource is not an instance of " + iface);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public boolean isWrapperFor(Class<?> iface) throws SQLException
   {
      if (iface.isInstance(this))
      {
         return true;
      }

      Pool p = pool;
      if (p != null)
      {
         final DataSource unwrappedDataSource = p.getUnwrappedDataSource();
         if (iface.isInstance(unwrappedDataSource))
         {
            return true;
         }

         if (unwrappedDataSource != null)
         {
            return unwrappedDataSource.isWrapperFor(iface);
         }
      }

      return false;
   }

   /**
    * Returns {@code true} if the proxy as been started and is not suspended or shutdown.
    *
    * @return {@code true} if the proxy as been started and is not suspended or shutdown.
    */
   public boolean isRunning()
   {
      return pool != null && pool.poolState == Pool.POOL_NORMAL;
   }


   /**
    * Evict a connectionProxy from the proxy.  If the connectionProxy has already been closed (returned to the proxy)
    * this may result in a "soft" eviction; the connectionProxy will be evicted sometime in the future if it is
    * currently in use.  If the connectionProxy has not been closed, the eviction is immediate.
    *
    * @param connection the connectionProxy to evict from the proxy
    */
   public void evictConnection(Connection connection)
   {
      Pool p;
      if (!isClosed() && (p = pool) != null && connection.getClass().getName().startsWith("com.zaxxer.hikari"))
      {
         p.evictConnection(connection);
      }
   }

   /**
    * Shutdown the DataSource and its associated proxy.
    */
   @Override
   public void close()
   {
      if (isShutdown.getAndSet(true))
      {
         return;
      }

      Pool p = pool;
      if (p != null)
      {
         try
         {
            LOGGER.info("{} - Shutdown initiated...", getPoolName());
            p.shutdown();
            LOGGER.info("{} - Shutdown completed.", getPoolName());
         } catch (InterruptedException e)
         {
            LOGGER.warn("{} - Interrupted during closing", getPoolName(), e);
            Thread.currentThread().interrupt();
         }
      }
   }

   /**
    * Determine whether the ConnectionPool has been closed.
    *
    * @return true if the ConnectionPool has been closed, false otherwise
    */
   public boolean isClosed()
   {
      return isShutdown.get();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String toString()
   {
      return "ConnectionPool (" + pool + ")";
   }
}
